/*
 * Copyright 2025 the original author or authors.
 * <p>
 * Licensed under the Moderne Source Available License (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * https://docs.moderne.io/licensing/moderne-source-available-license
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {AfterRecipeText, dedentAfter, SourceSpec} from "../test";
import {JavaScriptParser} from "./parser";
import {PackageJsonParser} from "./package-json-parser";
import {JS} from "./tree";
import ts from 'typescript';
import {json, Json} from "../json";
import {DependencyWorkspace} from "./dependency-workspace";
import {setTemplateSourceFileCache} from "./templating/engine";
import * as fs from "fs";
import * as path from "path";

/**
 * Shared TypeScript source file cache for test parsers.
 * Automatically configured for template parsing in tests.
 */
export const sourceFileCache: Map<string, ts.SourceFile> = new Map();

// Automatically enable sourceFileCache for template parsing in tests
setTemplateSourceFileCache(sourceFileCache);

export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]): AsyncGenerator<SourceSpec<any>, void, unknown> {
    // Ensure the target directory exists
    if (!fs.existsSync(relativeTo)) {
        fs.mkdirSync(relativeTo, { recursive: true });
    }

    // Find package.json and package-lock.json specs
    const packageJsonSpec = sourceSpecs.find(spec => spec.path === 'package.json');
    const packageLockSpec = sourceSpecs.find(spec => spec.path === 'package-lock.json');

    // Write package-lock.json first (if provided) so PackageJsonParser can read it
    if (packageLockSpec && packageLockSpec.before) {
        fs.writeFileSync(
            path.join(relativeTo, 'package-lock.json'),
            packageLockSpec.before
        );
    }

    // Yield package.json FIRST so its PackageJsonParser is used for all JSON specs
    // (The test framework uses the first spec's parser for all specs of the same kind)
    if (packageJsonSpec) {
        // Parse package.json to check if there are dependencies
        const packageJsonContent = JSON.parse(packageJsonSpec.before!);
        const hasDependencies = Object.keys({
            ...packageJsonContent.dependencies,
            ...packageJsonContent.devDependencies
        }).length > 0;

        // Get or create cached workspace with node_modules
        // If packageLockSpec is provided, use it for deterministic installs with npm ci
        if (hasDependencies) {
            const cachedWorkspace = await DependencyWorkspace.getOrCreateWorkspace({
                packageJsonContent: packageJsonSpec.before!,
                packageLockContent: packageLockSpec?.before ?? undefined
            });

            // Symlink node_modules from cached workspace to test directory
            const cachedNodeModules = path.join(cachedWorkspace, 'node_modules');
            const testNodeModules = path.join(relativeTo, 'node_modules');

            // Remove existing node_modules if present
            if (fs.existsSync(testNodeModules)) {
                fs.rmSync(testNodeModules, { recursive: true, force: true });
            }

            // Create symlink
            fs.symlinkSync(cachedNodeModules, testNodeModules, 'junction');
        }

        // Write the actual package.json from the test spec
        fs.writeFileSync(
            path.join(relativeTo, 'package.json'),
            packageJsonSpec.before!
        );

        // Use PackageJsonParser to parse and attach NodeResolutionResult marker
        yield {
            ...packageJsonSpec,
            parser: () => new PackageJsonParser({relativeTo})
        };
    }

    // Yield package-lock.json after package.json (so PackageJsonParser is used as the group parser)
    if (packageLockSpec) {
        yield packageLockSpec;
    }

    for (const spec of sourceSpecs) {
        if (spec.path !== 'package.json' && spec.path !== 'package-lock.json') {
            if (spec.kind === JS.Kind.CompilationUnit) {
                yield {
                    ...spec,
                    parser: () => new JavaScriptParser({sourceFileCache, relativeTo})
                }
            } else {
                yield spec;
            }
        }
    }
}

export function packageJson(before: string, after?: AfterRecipeText): SourceSpec<Json.Document> {
    return {
        ...json(before, after),
        path: 'package.json'
    };
}

export function packageLockJson(before: string, after?: AfterRecipeText): SourceSpec<Json.Document> {
    return {
        ...json(before, after),
        path: 'package-lock.json'
    };
}

export function javascript(before: string | null, after?: AfterRecipeText): SourceSpec<JS.CompilationUnit> {
    return {
        kind: JS.Kind.CompilationUnit,
        before: before,
        after: dedentAfter(after),
        ext: 'js',
        parser: ctx => new JavaScriptParser({sourceFileCache})
    };
}

export function typescript(before: string | null, after?: AfterRecipeText): SourceSpec<JS.CompilationUnit> {
    return {
        kind: JS.Kind.CompilationUnit,
        before: before,
        after: dedentAfter(after),
        ext: 'ts',
        parser: () => new JavaScriptParser({sourceFileCache})
    };
}

export function tsx(before: string | null, after?: AfterRecipeText): SourceSpec<JS.CompilationUnit> {
    return {
        kind: JS.Kind.CompilationUnit,
        before: before,
        after: dedentAfter(after),
        ext: 'tsx',
        parser: () => new JavaScriptParser({sourceFileCache})
    };
}

export function jsx(before: string | null, after?: AfterRecipeText): SourceSpec<JS.CompilationUnit> {
    return {
        kind: JS.Kind.CompilationUnit,
        before: before,
        after: dedentAfter(after),
        ext: 'jsx',
        parser: () => new JavaScriptParser({sourceFileCache})
    };
}
